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.repo_chosen, cfg, layout) {
138 Ok(tx) => {
139 let fetch_outcome = {
140 let _progress = crate::progress::start(
143 "Unpacking objects",
144 None,
145 crate::progress::should_report(quiet),
146 );
147 remote_dispatch::fetch_all_with(cwd, tx.as_ref(), &resolved.name, require_signed)
148 };
149 match fetch_outcome {
150 Ok(_) => {
151 let after = tracking_snapshot(layout, &resolved.name);
152 report_fetch(endpoint, &resolved.name, &before, &after);
153 if json {
154 emit_fetch_json(&resolved.name, endpoint, &before, &after);
155 }
156 exit::OK
157 }
158 Err(remote_dispatch::DispatchError::Interrupted) => {
159 emit_err_json("fetch: interrupted", exit::TEMPFAIL, json)
160 }
161 Err(e @ remote_dispatch::DispatchError::UnsignedOrInvalidObject { .. }) => {
162 emit_err_json(&format!("fetch: {e}"), exit::DATAERR, json)
163 }
164 Err(e) => emit_err_json(&format!("fetch: {e}"), exit::GENERAL_ERROR, json),
165 }
166 }
167 Err(remote_dispatch::DispatchError::UntrustedRemote(msg)) => {
168 emit_err_json(&msg, exit::CONFIG_ERROR, json)
169 }
170 Err(e) => emit_err_json(&format!("open remote: {e}"), exit::PROTOCOL_ERROR, json),
171 }
172}
173
174fn emit_fetch_json(
177 remote: &str,
178 endpoint: &str,
179 before: &HashMap<String, Hash>,
180 after: &HashMap<String, Hash>,
181) {
182 let mut changed: Vec<(&String, Option<Hash>, Hash)> = after
183 .iter()
184 .filter(|(name, new)| before.get(*name) != Some(*new))
185 .map(|(name, new)| (name, before.get(name).copied(), *new))
186 .collect();
187 changed.sort_by(|a, b| a.0.cmp(b.0));
188 let entries: Vec<String> = changed
189 .iter()
190 .map(|(name, old, new)| {
191 let mut obj = JsonObject::new();
192 obj.field_str("name", name)
193 .field_opt_hash("old", old.as_ref())
194 .field_hash("new", new);
195 obj.finish()
196 })
197 .collect();
198 let mut top = JsonObject::new();
199 top.field_bool("ok", true)
200 .field_str("remote", remote)
201 .field_str("endpoint", endpoint)
202 .field_raw("updated", &format!("[{}]", entries.join(",")));
203 let mut stdout = std::io::stdout().lock();
204 let _ = writeln!(stdout, "{}", top.finish());
205}
206
207fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
210 if json {
211 let mut obj = JsonObject::new();
212 obj.field_bool("ok", false).field_str("error", msg);
213 let mut stdout = std::io::stdout().lock();
214 let _ = writeln!(stdout, "{}", obj.finish());
215 }
216 emit_err(msg, code)
217}
218
219fn tracking_snapshot(layout: &RepoLayout, remote: &str) -> HashMap<String, Hash> {
222 mkit_core::refs::list_remote_refs(layout, remote)
223 .unwrap_or_default()
224 .into_iter()
225 .filter_map(|r| r.hash.map(|h| (r.name, h)))
226 .collect()
227}
228
229fn report_fetch(
232 endpoint: &str,
233 remote: &str,
234 before: &HashMap<String, Hash>,
235 after: &HashMap<String, Hash>,
236) {
237 let mut changed: Vec<(&String, Option<Hash>, Hash)> = after
238 .iter()
239 .filter(|(name, new)| before.get(*name) != Some(*new))
240 .map(|(name, new)| (name, before.get(name).copied(), *new))
241 .collect();
242 if changed.is_empty() {
243 return;
244 }
245 changed.sort_by(|a, b| a.0.cmp(b.0));
246 let mut stderr = std::io::stderr().lock();
247 let _ = writeln!(stderr, "From {endpoint}");
248 for (name, old, new) in changed {
249 let dst = format!("{remote}/{name}");
253 let _ = writeln!(
254 stderr,
255 "{}",
256 format::ref_update_line(old.as_ref(), &new, name, &dst, false)
257 );
258 }
259}
260
261use super::error as emit_err;